package roborally.model;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import roborally.exceptions.InvalidPositionException;
import roborally.exceptions.PositionSharingException;

import be.kuleuven.cs.som.annotate.*;

/**
 * A class for dealing with boards.
 * 
 * @invar	...
 *          | canHaveAsWidth(getWidth())
 * @invar	...
 *          | canHaveAsHeigth(getHeight())
 * 
 * @version 1.0
 * @author  Pieter Robberechts	(1e Bach Informatica)
 * @author	Jeroen Sanders		(1e Bach Informatica)
 */

public class Board extends Terminatable{

	/**
	 * Initialize this new board with given width and height.
	 * 
	 * @param	width
	 * 			The width for the board.
	 * @param 	height
	 * 			The height for the board.
	 * @post	...
	 * 			| new.getWidth() == width
	 * @effect	...
	 * 			| new.getHeight() == height
	 * @throws	IllegalArgumentException
	 * 			The given width is invalid.
	 * 			| ! canHaveAsWidth(width)
	 * @throws	IllegalArgumentException
	 * 			The given height is invalid.
	 * 			| ! canHaveAsHeight(height)
	 * */
	public Board(long width, long height) throws IllegalArgumentException{
		if(canHaveAsWidth(width) && canHaveAsHeight(height)){
			this.width = width;
			this.height = height;
		}else
			throw new IllegalArgumentException("Invalid borders!");
	}
	
	/**
	 * Return the maximal width for each board.
	 */
	@Basic @Immutable
	public static long getMaxWidth(){
		return Long.MAX_VALUE;
	}
	
	/**
	 * Return the maximal height for each board.
	 */
	@Basic @Immutable
	public static long getMaxHeight(){
		return Long.MAX_VALUE;
	}
	
	/**
	 * Check whether the given width is a valid width for a board.
	 * 
	 * @param 	width
	 *         	The width to check.
	 * @return 	...
	 *       	| result == ((width >= 0) && (width <= getMaxWidth()))
	 */
	public boolean canHaveAsWidth(long width){
		return ((width >= 0) && (width <= getMaxWidth()));
	}
	
	/**
	 * Check whether the given height is a valid height for a board.
	 * 
	 * @param 	height
	 *         	The height to check.
	 * @return 	...
	 *       	| result == ((height >= 0) && (height <= getMaxHeight()))
	 */
	public boolean canHaveAsHeight(long height){
		return ((height >= 0) && (height <= getMaxHeight()));
	}
	
	/**
	 * Return the width of this board.
	 * 
	 */
	@Basic @Immutable
	public long getWidth(){
		return width;
	}

	/**
	 * Variable registering the width of this board.
	 */
	private final long width;
	
	/**
	 * Return the height of this board.
	 * 
	 */
	@Basic @Immutable
	public final long getHeight(){
		return height;
	}
	
	/**
	 * Variable registering the height of this board.
	 */
	private long height;
	
	/**
	 * Check whether the given position is a valid position for any object 
	 * 
	 * @param 	position
	 * 			The position to check.	
	 * @return	...
	 * 			| result == position != null && position.getX() <= getWidth() && position.getY() <= getHeight()
	 */
	public boolean isValidPosition(Position position){
		return position != null && position.getX() <= getWidth() && position.getY() <= getHeight();
	}
	
	 /**
     * Variable referencing a map collecting all the elements
     * present on this board.
     * 
     * @invar   The referenced map is effective.
     *        | elementsOnBoard != null
     * @invar   Each key registered in the map is an effective position.
     *        | for each key in elementsOnBoard.keySet():
     *        |   (key != null)
     * @invar   Each value associated with a key in the map is a set
     *          of effective, non-terminated elements which can share their position 
     *          with all the other elements in the set involving this
     *          board and involving a position which is identical
     *          to the associating key.
     *        | for each key in elementsOnBoard.keySet():
     *        |   	(elementsOnBoard.get(key) != null) 
     *        |		for each element in elementsOnBoard.get(key)	
     *        |   		(! element.isTerminated()) &&
     *        |   		(element.getBoard() == this) &&
     *        |   		(element.getPosition().equals(key))
     */
	private Map<Position, Set<Element>> elementsOnBoard = new HashMap<Position, Set<Element>>();
	
	/**
	 * Return a set of all elements of a given class which appear on the board.
	 * 
	 * @param 	c
	 * 			Search all elements which belong to this class.
	 * @return	...
	 * 			| for each set in elementsOnBoard.values():
	 * 			|	for each element in set:
	 * 			|		if(element.getClass == c)
	 * 			|			then result == element
	 * 
	 */
	@SuppressWarnings("unchecked")
	public <T extends Element> Set<T> getElements(Class<T> c) {
		Set<T> result = new HashSet<T>();
		Iterator<Set<Element>> sets = elementsOnBoard.values().iterator();
		while( sets.hasNext() ) {
			Iterator<Element> elements = sets.next().iterator();
			while( elements.hasNext() ) {
				Element element = elements.next();
				if(element.getClass() == c)
					result.add((T) element);
			}
		}	
		return result;
	}
	
	/**
	 * Return a set of all elements which are located on the board.
	 * 
	 * @return	...
	 * 			| for each set in elementsOnBoard.values():
	 * 			|	for each element in set:
	 * 			|		result == element
	 */
	public Iterable<Element> getAllElements(){
		Set<Element> result = new HashSet<Element>();
		
		Iterator<Set<Element>> sets = elementsOnBoard.values().iterator();
		while( sets.hasNext() ) {
			Iterator<Element> elements = sets.next().iterator();
			while( elements.hasNext() ) {
				Element element = elements.next();
					result.add(element);
			}
		}	
		return result;
	}
	
	/**
	 * Return a set of all elements which are located on a given position on the board.
	 * 
	 * @param 	position
	 * 			Search all elements which are located on this position.
	 * @return 	...
	 * 			| result == elementsOnBoard.get(position)
	 * @throws	InvalidPositionException
	 * 			The given position is invalid for this board.
	 * 			| ! isValidPosition(position)
	 */
	public Iterable<Element> getElements(Position position) throws InvalidPositionException{
		if (!isValidPosition(position))
			throw new InvalidPositionException(position);
		
		if(elementsOnBoard.get(position) == null)
			return new HashSet<Element>();
		else	
			return elementsOnBoard.get(position);
	}
	
	/**
	 * Put a given element, on a given position on the board.
	 * 	When an element is located on another board, remove it from this board first.
	 * 	Replace the element if it is already on this board, but on another position. 
	 * 
	 * @param 	position
	 * 			The position to put the element on.
	 * @param 	element
	 * 			The element to put on the board.
	 * @post	...
	 * 			| for one puttedElement in getElements(position):
	 * 			|	puttedElement == element
	 * @effect	...
	 * 			| element.setPosition(position)
	 * @effect	...
	 * 			| element.setBoard(this)
	 * @throws	IllegalStateException
	 * 			The given element is terminated.
	 * 			| element.isTerminated()
	 * @throws	IllegalArgumentException
	 * 			The given element is ineffective.
	 * 			| element == null	
	 * @throws	IllegalArgumentException
	 * 			The given position is ineffective.
	 * 			| position == null
	 * @throws 	InvalidPositionException
	 * 			The given position is invalid for this board.
	 * 			| ! isValidPosition(Position position)	
	 * @throws 	PositionSharingException 
	 * 			The given element can't share its position with one of the elements already located on the given position.
	 * 			| !canHaveElementOnPosition(element, position)	 
	 */
	public void putElement(Position position, Element element) throws IllegalStateException, IllegalArgumentException, InvalidPositionException, PositionSharingException{
		if(element == null)
			throw new IllegalArgumentException("Ineffective element!");
		if(position == null)
			throw new IllegalArgumentException("Ineffective position!");
		if(element.getBoard() == this && element.getPosition().equals(position))
			return;
		if(element.isTerminated())
			throw new IllegalStateException("This element is terminated!");
		if(!canHaveElementOnPosition(element, position))
			throw new PositionSharingException("The given element and one or more elements on the given position can't share the same position.");
		
		if(element.getBoard() != null){
			element.getBoard().removeElement(element);
		}
		
		if(elementsOnBoard.containsKey(position)){
			elementsOnBoard.get(position).add(element);
		}else{
			Set<Element> set = new HashSet<Element>();
			set.add(element);
			elementsOnBoard.put(position, set);
		}
		
		element.setBoard(this);
		element.setPosition(position);
	}
	
	/**
	 * Remove the given element from the board.
	 * 	If the element isn't present on the board, do nothing.
	 * 
	 * @param 	element
	 * 			The element to remove from the board.
	 * @post	...
	 * 			| isOnBoard(element) == false
	 * @effect	...
	 * 			| element.setPosition(null)
	 * @effect	...
	 * 			| element.setBoard(null)
	 * @throws	IllegalArgumentException
	 * 			The given element is ineffective.
	 * 			| element == null	
	 * @throws 	IllegalArgumentException
	 * 			The given element isn't located on this board.
	 * 			| !elementsOnBoard.containsKey(element.getPosition()) || !elementsOnBoard.get(element.getPosition()).contains(element) 
	 * 			
	 */
	public void removeElement(Element element) throws IllegalArgumentException{
		if(element == null)
			throw new IllegalArgumentException("Ineffective element!");
		if(!elementsOnBoard.containsKey(element.getPosition()) || !elementsOnBoard.get(element.getPosition()).contains(element))
			throw new IllegalArgumentException("The given element isn't located on this board.");
		
		elementsOnBoard.get(element.getPosition()).remove(element);
		if(elementsOnBoard.get(element.getPosition()).isEmpty())
			elementsOnBoard.remove(element.getPosition());	
		
		try {
			element.setBoard(null);
			element.setPosition(null);
		} catch (IllegalStateException e) {
			e.printStackTrace();
		} catch (InvalidPositionException e) {
			e.printStackTrace();
		}
			
	}
	
	/**
	 * Get the first  element encountered while looping trough the board,
	 * starting from a given position in a given direction.
	 *  
	 * @param 	position
	 * 			The position to start searching from.
	 * @param 	orientation
	 * 			The direction to loop trough the board while searching.
	 * @return	...
	 * 			| for one positionX, for each positionY : 
	 * 			|	if( isValidPosition(positionX) && isValidPosition(positionY) && 
	 * 			|		position.isPlacedAtOrientationOf(positionX, orientation) && position.isPlacedAtOrientationOf(positionX, orientation) &&
	 * 			|		elementsOnBoard.containsKey(positionX) && elementsOnBoard.containsKey(positionY) &&
	 * 			|		position.getManhattanDistance(positionX) <= position.getManhattanDistance(positionY) )
	 * 			|	then 
	 * 			|		result == positionX
	 * @return	...
	 * 			| for each positionX : 
	 * 			| 	if( isValidPosition(positionX) &&
	 * 			|		position.isPlacedAtOrientationOf(positionX, orientation) && 
	 * 			|		! elementsOnBoard.containsKey(positionX) )
	 * 			|	then 
	 * 			|		result == null
	 * @throws	IllegalArgumentException
	 * 			The given position is ineffective.
	 * 			| position == null
	 * @throws	IllegalArgumentException
	 * 			The given orientation is ineffective.
	 * 			| orientation == null	
	 * @throws 	InvalidPositionException
	 * 			The given position is invalid.
	 * 			| !isValidPosition(position)
	 */
	public Element getFirstElementStartingAt(Position position, Orientation orientation) throws IllegalArgumentException, InvalidPositionException {
		if(position == null)
			throw new IllegalArgumentException("Ineffective position!");
		if(orientation == null)
			throw new IllegalArgumentException("Ineffective orientation!");
		if(!isValidPosition(position))
			throw new InvalidPositionException(position);
		
		try{
			do
				position = position.getNeighbour(orientation); 
			while(!elementsOnBoard.containsKey(position) && isValidPosition(position));
		}catch(InvalidPositionException e){
			return null;
		}	
		
		if(isValidPosition(position))
			return elementsOnBoard.get(position).iterator().next();
		else
			return null;
	}

	/**
	 * Checks if the given element can have the given position as position on this board.
	 * 
	 * @param	position
	 * 			The position that needs to be checked
	 * @param	element
	 * 			The element for which the position must be valid.
	 * @return	...
	 * 			| result == element.canSharePositionWith(getElements(position))
	 * @throws	IllegalArgumentException
	 * 			The given position is ineffective.
	 * 			| position == null
	 * @throws	IllegalArgumentException
	 * 			The given element is ineffective.
	 * 			| element == null
	 * @throws 	InvalidPositionException 
	 * 			The given position is invalid.
	 * 			| ! isValidPosition(position)
	 */
	public boolean canHaveElementOnPosition(Element element, Position position) throws IllegalArgumentException, InvalidPositionException {
		if(element == null)
			throw new IllegalArgumentException("Ineffective element!");
		if(position == null)
			throw new IllegalArgumentException("Ineffective position!");
		if(!isValidPosition(position))
			throw new InvalidPositionException(position);
		
		return element.canSharePositionWith(getElements(position));
	}
	
	/**
	 * Check whether a board can be merged with a second board.
	 * 
	 * @param 	other
	 * 			The second board, which has to be compared with the prime board. 
	 * @return 	...
	 * 			| result == (this != other && other != null && !isTerminated() && !other.isTerminated())
	 */
	public boolean isCompatibleWith(Board other){
		return (this != other && other != null && !isTerminated() && !other.isTerminated());
	}
	
	/**
	 * Merge two boards.
	 * 	All robots, walls and batteries from the second board 
	 * 	are moved (whenever possible) to the first board at the corresponding positions.
	 * 
	 * @param 	other
	 * 			The second board which has to be merged with the prime board.
	 * @effect	...
	 * 			| for each element in other.getAllElements():
	 * 			|	putElement(element.getPosition(), element)
	 * @effect	...
	 * 			| other.terminate()
	 * @throws	IllegalArgumentException
	 * 			Both boards are incompatible.
	 * 			| !isCompatibleWith(other)
	 */
	public void merge(Board other) throws IllegalArgumentException{
		if(!isCompatibleWith(other))
			throw new IllegalArgumentException("Both boards aren't compatible.");

		for(Element element : other.getAllElements() ) {
			try {
				putElement(element.getPosition(), element);
			} catch (IllegalArgumentException e) {
				// Do nothing
			} catch (InvalidPositionException e) {
				// Do nothing
			} catch (PositionSharingException e) {
				// Do nothing
			}
		}

		other.terminate();
	}
	
	/**
	 * Terminate a board.
	 * 	When a board is terminated, all robots, walls and batteries on that board are terminated as well.
	 * 
	 * @effect	...
	 * 			| super.terminate()
	 * @effect	...
	 * 			| for each set in elementsOnBoard.values():
	 * 			|	for each element in set:
	 * 			|		element.terminate()
	 *  
	 */
	@Override
	public void terminate(){		
		Iterator<Element> elements = getAllElements().iterator();
		while(elements.hasNext())
			elements.next().terminate();
		
		super.terminate();
	}
}